# -*- coding: utf-8 -*-

from hlm.port import PortIn, PortOut


class LaneCheck:
    """ This class provides the type check for Lane related classes. If the
    given variable does not matching the type, an exception will be raised.
    """
    @staticmethod
    def check_lane_port_sender(lane_sender,
                               error_msg='{0} is not a lane sender port'):
        """ Check whether the variable is a LanePortIn type.
        Args:
            lane_sender: The tested variable.
            error_msg: The customized error message.
        """
        if not isinstance(lane_sender, LanePortSender):
            raise Exception(error_msg.format(str(lane_sender)))

    @staticmethod
    def check_lane_port_receiver(lane_receiver,
                                 error_msg='{0} is not a lane receiver port'):
        """ Check whether the variable is a LanePortOut type.
            Args:
                lane_receiver: The tested variable.
                error_msg: The customized error message.
        """
        if not isinstance(lane_receiver, LanePortReceiver):
            raise Exception(error_msg.format(str(lane_receiver)))


class LanePort:
    """ LanePort is an abstract class for Lane level ports. It is used for port
    type checking. There is no useful function in this abstract class. It
    provides several abstract method for the LanePort type instance.
    """
    def port_names(self) -> list:
        """ Get the port name list of the LanePort instance.
        Returns: The list of the port name.
        """
        return []


class LanePortReceiver(LanePort):
    """ LanePortReceiver provides the abstract port of the receiver parts of the
    Lane level connection. It automatically manages several PortIn class of a
    Node instance.
    """
    def __init__(self, port_info_list: list, get_format: list):
        """Create the receiver side port. Please noticed, the name in port info
        list should contains the name of the node as the suffix.
        Args:
            port_info_list: The list of the port information, which is used to
            create the port.
            get_format: The value format returned order of the `get()` method.
        """
        # Save the get entry method.
        self.__get_format = get_format
        # Generate an empty port list.
        self.__port_dict = {}
        # Initial the ports.
        for port_info in port_info_list:
            # Get the suffix port info.
            name = ''
            port = None
            # Check port info types.
            if isinstance(port_info, str):
                # For string type, just simply add suffix to the name.
                name = port_info
                port = PortIn()
            elif isinstance(port_info, set) or isinstance(port_info, tuple):
                # Extract the name
                name = port_info[0]
                # Get the port.
                port = PortIn(
                    max_size=-1 if len(port_info) == 1 else int(port_info[1]))
            elif isinstance(port_info, dict):
                # Check whether the dictionary contains max_size key.
                if 'max_size' not in port_info:
                    # Set the maximum size key.
                    port_info['max_size'] = -1
                # Extract the name and port.
                name = port_info['name']
                port = PortIn(max_size=port_info['max_size'])
            # Add port to the managing port list.
            self.__port_dict[name] = port
            # Set the lane port in attributes.
            setattr(self, name, port)

    def port_names(self) -> list:
        """ Get the port name list of the LanePortReceiver instance.
        Returns: The list of the port name.
        """
        return list(self.__port_dict.keys())

    def fetch_ports(self) -> dict:
        """ Fetch all items from the port, and get a dictionary of the fetch
        result.
        Returns: A dictionary for fetch all the data once from the ports.
        """
        # Default get dictionary.
        port_result = {}
        # Loop in the port list:
        for port_name in self.__port_dict:
            # Get the port data.
            port_result[port_name] = self.__port_dict[port_name].get()
        # Give back the result.
        return port_result

    def get(self) -> any:
        """ Based on the get port list, return the result. If the get port list
        is empty, the port get result dictionary would return.
        Returns: The managing ports get result.
        """
        # Start to get the value from ports.
        if len(self.__get_format) == 0:
            return self.fetch_ports()
        # Or else based on the format type, give the result back
        # Prepare the return list.
        result_list = []
        # Loop and get each item from the list.
        for item in self.__get_format:
            # Directly get the result.
            result_list.append(self.__port_dict[item].get())
        # Check for special condition, if only 1 item in the returned value,
        # extract that value out.
        if len(result_list) == 1:
            return result_list[0]
        # Otherwise, directly return the result.
        return result_list

    def is_any_empty(self) -> bool:
        """ Check whether any input port of the LanePortIn is empty.
        Returns: If any port is empty, return True, otherwise False.
        """
        # Loop and check each port in the list.
        for port_name in self.__port_dict:
            # Check the port is empty.
            if self.__port_dict[port_name].is_empty():
                # Find one of the node is empty.
                return True
        # All ports are not empty.
        return False

    def is_any_full(self) -> bool:
        """ Check whether any input port of the LanePortIn is full.
        Returns: If any port is full, return True, otherwise False.
        """
        # Loop and check each port in the list.
        for port_name in self.__port_dict:
            # Check the port is empty.
            if self.__port_dict[port_name].is_full():
                # Find one of the node is empty.
                return True
        # All ports are not empty.
        return False


class LanePortSender(LanePort):
    """ LanePortSender provides the abstraction of the sender side of the port
    set. All the ports in the LanePortSender are PortOut type.
    """
    def __init__(self, port_list: list):
        # Create the port dictionary.
        self.__port_dict = {}
        # Initial the ports.
        for port_name in port_list:
            # Generate the PortOut instance.
            port = PortOut()
            # Add the port to the list.
            self.__port_dict[port_name] = port
            # Set the attributes for the current lanes.
            setattr(self, port_name, port)

    def port_names(self) -> list:
        """ Get the port name list of the LanePortSender instance.
        Returns: The list of the port name.
        """
        return list(self.__port_dict.keys())

    def put(self, **port_var_dict: dict) -> None:
        """ Put the value based on the value dict to the PortOut instances.
        Args:
            port_var_dict: The value needs to be put for all the PortOut
            instances.
        """
        # Loop in the port out list.
        for port_name in port_var_dict:
            # Ensure the port name validation.
            if port_name not in self.__port_dict:
                # Ignore the port name set.
                continue
            # Get the port from the node.
            self.__port_dict[port_name].put(port_var_dict[port_name])

    def put_bypass(self, port_receiver: LanePortReceiver) -> None:
        """ Put a received value from the same port, getting the data from the
        receive port and pass to the same port.
        Args:
            port_receiver: The receiver port which provides the data to pass.
        Returns:
        """
        # Fetch the result from the receiver.
        fetch_result = port_receiver.fetch_ports()
        # Loop for all the port names.
        for port_name in self.port_names():
            # Passing the port data.
            self.__port_dict[port_name].put(fetch_result[port_name])

    def is_any_full(self) -> bool:
        """ Check whether one of the PortOut instance marks as full.
        Returns: If any one of the managing PortOut instance marks as full,
        return True. Otherwise return False.
        """
        # Check either one of the out port are full.
        for node_port_name in self.__port_dict:
            # Check the port out.
            if self.__port_dict[node_port_name].is_full():
                # One port out is full detected,
                return True
        # No port is full.
        return False


class Lane:
    """ On the high-level, a Lane class is a wrapper to I/O ports between two
    connected nodes, it defines a connection from an input port to an output
    port. The explicit relation of such instances could be 1:1 for 1:N, but
    always directing from the output port to the input(s). User only needs to
    provide the port name for matching.
    Inside a specific Lane, all the data directions are the same. All of them
    are from a set of PortOut of one node to a set of PortIn of the other node.
    Lane supports for adding suffix to the end of the ports for multiple times
    usage on both sender and receiver sides.
        Examples:
            To setup a Lane, the `ports` variable needs to be initialized for
            one type of the Lane. It supports for three working ways::
                ports = ['d1',          # port with no max_size limit
                         {'name': 'd2', # connect ports with explicit definition
                          'max_size': 2,
                          'type': int},
                         {'name': 'd3'},
                         ('d4', 2),     # connect ports using tuple definition
                         ('d5', 2, str)
                         ]
            Then, you could customized the value returned order for calling the
            `get()` method of the lane instance. If this list is empty, a
            dictionary will be returned with the name of the port matching its
            value. Assign the `get_format` of the Lane type could let the return
            value of the `get()` method to be a tuple at a specific order. For
            example::
                get_format = ['d_in2', 'd_in1', 'd_in3']
    """
    ports = []
    get_format = []

    @staticmethod
    def get_port_name_list(port_list: list) -> list:
        # Loop and modify the name of the ports.
        node_ports = []
        for port_info in port_list:
            # Check port info types.
            if isinstance(port_info, str):
                # Append the string directly to the node ports.
                node_ports.append(port_info)
            elif isinstance(port_info, set) or isinstance(port_info, tuple):
                # Append the first element to the list.
                node_ports.append(port_info[0])
            elif isinstance(port_info, dict):
                # Append the data to the port.
                node_ports.append(port_info['name'])
        # Give back the node ports.
        return node_ports

    @classmethod
    def get_receiver(cls) -> LanePortReceiver:
        # Based on the port, generate the port.
        return LanePortReceiver(cls.ports, cls.get_format)

    @classmethod
    def get_sender(cls) -> LanePortSender:
        # Based on the port, generate the port.
        return LanePortSender(Lane.get_port_name_list(cls.ports))

    @staticmethod
    def connect_ports(ports: list,
                      sender: LanePortSender,
                      receiver: LanePortReceiver) -> None:
        # Check port in and port out type.
        LaneCheck.check_lane_port_sender(sender)
        LaneCheck.check_lane_port_receiver(receiver)
        # Get the port name and suffix port name.
        port_name_list = Lane.get_port_name_list(ports)
        # Connect the ports from the sender to receiver.
        for port_name in port_name_list:
            # Get the port from sender and connect the port to receiver.
            getattr(sender, port_name).connect(getattr(receiver, port_name))

    @classmethod
    def connect(cls,
                sender: LanePortSender,
                receiver: LanePortReceiver) -> None:
        # Connect the ports using the port list inside the class.
        Lane.connect_ports(cls.ports, sender, receiver)